<!DOCTYPE html>
<html>
  <head>
     
    <meta charset="UTF-8">
    <title>JVM - 测开、Java后端最常见、最核心的面试题总结</title>
    <link rel="shortcut icon" href="/static/img/icon.png">
    <link rel="icon" href="/static/img/icon.png" sizes="192x192"/>
    
<link rel="stylesheet" href="/static/kico.css">
<link rel="stylesheet" href="/static/hingle.css">

    
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">

    <meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1"/>
    <meta property="og:site_name" content="测开、Java后端最常见、最核心的面试题总结">
    <meta property="og:title" content="JVM"/>
    
<meta name="generator" content="Hexo 7.3.0"></head>

  <body>
    <header>
    <div class="head-title">
        <h4>测开、Java后端最常见、最核心的面试题总结</h4>
    </div>
    <div class="head-action">
        <div class="toggle-btn"></div>
        <div class="light-btn"></div>
        <div class="search-btn"></div>
    </div>
    <form class="head-search" method="post">
        <input type="text" name="s" placeholder="搜索什么？">
    </form>
    <nav class="head-menu">
        <a href="/">首页</a>
        
    </nav>
</header>

    <main>
    <div class="wrap min">
        <section class="post-title">
            <h2>JVM</h2>
            <div class="post-meta">
                <time class="date">2024.07.21</time>
            
            </div>
        </section>
        <article class="post-content">
        
            <h1><span id="1jvm-的组织架构">1.JVM 的组织架构</span></h1><p>JVM 大致可以划分为三个部门：类加载器、运行时数据区和执行引擎。</p>
<p>① 类加载器</p>
<p>负责加载 Class 文件，将 Class 文件中的二进制数据读入到内存当中。</p>
<p>② 运行时数据区</p>
<p>JVM 在执行 Java 程序时，需要在内存中分配空间来处理各种数据，这些内存区域主要包括方法区、堆、栈、程序计数器和本地方法栈。</p>
<p>③ 执行引擎</p>
<p>执行引擎是 JVM 的心脏，负责执行字节码。它包括一个虚拟处理器，还包括即时编译器（JIT Compiler）和垃圾回收器（Garbage Collector）。</p>
<h1><span id="2jvm-的内存区域">2.JVM 的内存区域</span></h1><p>JVM 的内存区域，有时叫 JVM 的内存结构，有时也叫 JVM 运行时数据区，按照 Java 的虚拟机规范，可以细分为<code>程序计数器</code>、<code>虚拟机栈</code>、<code>本地方法栈</code>、<code>堆</code>、<code>方法区</code>等。</p>
<p>其中<code>方法区</code>和<code>堆</code>是线程共享的，<code>虚拟机栈</code>、<code>本地方法栈</code>和<code>程序计数器</code>是线程私有的。</p>
<p><strong>程序计数器：</strong></p>
<p>程序计数器是一块较小的内存空间，可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令，分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。</p>
<p><strong>Java 虚拟机栈：</strong></p>
<p>Java 虚拟机栈（Java Virtual Machine Stack），通常指的就是“栈”，它的生命周期与线程相同。</p>
<p>当线程执行一个方法时，会创建一个对应的栈帧，用于存储局部变量表、操作数栈、动态链接、方法出口等信息，然后栈帧会被压入栈中。当方法执行完毕后，栈帧会从栈中移除。</p>
<p><strong>本地方法栈：</strong></p>
<p>本地方法栈（Native Method Stacks）与虚拟机栈相似，区别在于虚拟机栈是为 JVM 执行 Java 编写的方法服务的，而本地方法栈是为 Java 调用本地（native）方法服务的，由 C&#x2F;C++ 编写。</p>
<p>在本地方法栈中，主要存放了 native 方法的局部变量、动态链接和方法出口等信息。当一个 Java 程序调用一个 native 方法时，JVM 会切换到本地方法栈来执行这个方法。</p>
<p><strong>堆：</strong></p>
<p>Java 虚拟机所管理的内存中最大的一块，Java 堆是所有线程共享的一块内存区域，在虚拟机启动时创建。<strong>此内存区域的唯一目的就是存放对象实例，几乎所有的对象实例以及数组都在这里分配内存。</strong></p>
<p>Java 世界中“几乎”所有的对象都在堆中分配，但是，随着 JIT 编译器的发展与逃逸分析技术逐渐成熟，栈上分配、标量替换优化技术将会导致一些微妙的变化，所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析，如果某些方法中的对象引用没有被返回或者未被外面使用（也就是未逃逸出去），那么对象可以直接在栈上分配内存。</p>
<p><strong>方法区：</strong></p>
<p>方法区并不真实存在，属于 Java 虚拟机规范中的一个逻辑概念，方法区逻辑上属于堆，用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。<br><strong>方法区的实现：</strong><br><strong>永久代（Permanent Generation）</strong>和<strong>元空间（Metaspace）</strong>：</p>
<ul>
<li>在JDK 7及之前的版本中，有永久代（Permanent Generation），用于存储类的元数据（类信息、常量、静态变量等）。</li>
<li>从JDK 8开始，永久代被移除，取而代之的是元空间（Metaspace）。元空间不再使用堆内存，而是使用本地内存。元空间的大小默认是动态调整的，避免了永久代大小固定可能导致的内存溢出问题。</li>
</ul>
<p><strong>运行时常量池：</strong></p>
<p>属于方法区的一部分。它用于存放编译期生成的各种字面量和符号引用，这部分内容将在类加载后存放到方法区的运行时常量池中。</p>
<p><strong>字符串常量池：</strong></p>
<p>在Java中，字符串是常用的数据类型，因此专门为它们设计了一个字符串常量池。Java中的字符串常量池驻留在堆内存中。JVM在加载类的时候，如果检测到一个字符串字面量，就会先检查字符串常量池中是否存在这个字符串，如果存在就直接返回它的引用；如果不存在，就会将这个字符串实例化后放入常量池中。</p>
<h1><span id="3对象创建的过程">3.对象创建的过程</span></h1><p>当我们使用 new 关键字创建一个对象的时候，JVM 首先会检查 new 指令的参数是否能在常量池中定位到一个类的符号引用，然后检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有，就先执行相应的类加载过程。</p>
<p>如果已经加载，JVM 会为新生对象分配内存，内存分配完成之后，JVM 将分配到的内存空间初始化为零值（成员变量，数值类型是 0，布尔类型是 false，对象类型是 null），接下来设置对象头，对象头里包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。</p>
<p>最后，JVM 会执行构造方法，将成员变量赋值为预期的值，这样一个对象就创建完成了。</p>
<h1><span id="4垃圾回收机制">4.垃圾回收机制</span></h1><p>Java的垃圾回收机制（Garbage Collection，简称GC）是Java虚拟机的一项重要功能，它自动管理内存，通过回收不再被引用的对象，释放内存空间，从而避免内存泄漏和提升程序的稳定性。</p>
<h1><span id="5垃圾判断算法">5.垃圾判断算法</span></h1><ul>
<li>引用计数算法</li>
<li>可达性分析算法</li>
</ul>
<p><strong>引用计数算法</strong>是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用，则它的引用计数加 1，如果删除对该对象的引用，那么它的引用计数就减 1，当该对象的引用计数为 0 时，那么该对象就会被回收。</p>
<p>引用计数算法将垃圾回收分摊到整个应用程序的运行当中，而不是集中在垃圾收集时。引用计数算法看似很美好，但实际上它存在一个很大的问题，那就是无法解决循环依赖的问题。</p>
<p><strong>可达性分析算法</strong>的基本思路是，通过 GC Roots 作为起点，然后向下搜索，搜索走过的路径被称为引用链，当一个对象到 GC Roots 之间没有任何引用相连时，即从 GC Roots 到该对象节点不可达，则证明该对象是需要垃圾收集的。</p>
<p>通过可达性算法，成功解决了引用计数无法解决的问题-“循环依赖”，只要你无法与 GC Root 建立直接或间接的连接，系统就会判定你为可回收对象。</p>
<p>所谓的 GC Roots，就是一组必须活跃的引用，不是对象，它们是程序运行时的起点，是一切引用链的源头。在 Java 中，GC Roots 包括以下几种：</p>
<ul>
<li>虚拟机栈中的引用（方法的参数、局部变量等）</li>
<li>本地方法栈中 JNI 的引用</li>
<li>类静态变量</li>
<li>运行时常量池中的常量（String 或 Class 类型）</li>
</ul>
<h1><span id="6引用类型">6.引用类型</span></h1><ol>
<li><strong>强引用（Strong Reference）</strong>：<ul>
<li>这是最常见的引用类型，比如<code>Object obj = new Object();</code>中的<code>obj</code>就是一个强引用。</li>
<li>只要强引用存在，垃圾收集器就不会回收被引用的对象。</li>
</ul>
</li>
<li><strong>软引用（Soft Reference）</strong>：<ul>
<li>通过<code>java.lang.ref.SoftReference</code>类实现。</li>
<li>当系统内存不足时，垃圾收集器会回收软引用所指向的对象，即使存在强引用到这些对象。</li>
<li>软引用通常用于实现内存敏感的缓存。</li>
</ul>
</li>
<li><strong>弱引用（Weak Reference）</strong>：<ul>
<li>通过<code>java.lang.ref.WeakReference</code>类实现。</li>
<li>弱引用所指向的对象在下一次垃圾收集时就会被回收，无论是否存在其他的引用。</li>
<li>弱引用无法阻止对象的垃圾收集，它们常用于跟踪对象的生命周期，而不延长对象的生命周期。</li>
</ul>
</li>
<li><strong>虚引用（Phantom Reference）</strong>：<ul>
<li>通过<code>java.lang.ref.PhantomReference</code>类实现。</li>
<li>虚引用几乎不对对象的生命周期产生影响，它们主要用于跟踪对象被垃圾收集器回收的活动。</li>
</ul>
</li>
</ol>
<h1><span id="7垃圾回收算法">7.垃圾回收算法</span></h1><ul>
<li><strong>标记清除算法：</strong>最基础的一种垃圾回收算法，把内存区域中的这些对象进行标记，哪些属于可回收的标记出来（用前面提到的可达性分析法），然后把这些垃圾拎出来清理掉。缺点是标记和清除过程效率较低，并且会产生大量不连续的内存碎片。</li>
<li><strong>复制算法：</strong>将内存分为两块，每次只使用其中的一块。当这一块内存用完时，将还存活的对象复制到另一块内存中，然后清空已使用的内存块。优点是效率高、没有碎片问题，但可用内存减半。</li>
<li><strong>标记-整理算法：</strong>首先标记所有被引用的对象，然后将存活的对象移动到内存的一端，最后清理掉边界外的内存。结合了标记-清除和复制算法的优点，适用于老年代回收。</li>
<li><strong>分代收集算法：</strong>将堆内存分为新生代和老年代，新生代用复制算法，老年代用标记-清除或标记-整理算法。因为大多数对象在新生代被回收，只有少量存活对象进入老年代，这种方式提高了效率。</li>
</ul>
<p>在Java中，堆被分为几个区域，以便更高效地进行内存管理和垃圾回收。主要分为以下几个区域：</p>
<ol>
<li><strong>新生代（Young Generation）</strong>：<ul>
<li><strong>Eden区</strong>：大多数新对象在Eden区分配。Eden区的对象生命周期通常很短，会很快变为垃圾。</li>
<li><strong>Survivor区</strong>：分为两个相同大小的区域，称为Survivor 0（或S0）和Survivor 1（或S1）。在进行垃圾回收时，从Eden区和其中一个Survivor区（From区）复制存活的对象到另一个Survivor区（To区），并清空Eden区和From区。每次Minor GC后，两个Survivor区角色交换，即原来的To区变为新的From区。</li>
</ul>
</li>
<li><strong>老年代（Old Generation）</strong>：存放生命周期较长的对象。这些对象通常是从新生代晋升过来的。当对象在新生代经历了多次垃圾回收依然存活时，会被移动到老年代。老年代的垃圾回收频率较低，但回收时间通常较长。</li>
<li><strong>永久代（PermGen）或元空间（Metaspace）</strong><ul>
<li><strong>永久代</strong>（PermGen）：在Java 8之前，永久代用于存储类的元数据、常量池、方法信息等。它属于堆的一部分，但在Java 8之后被元空间取代。</li>
<li><strong>元空间</strong>（Metaspace）：从Java 8开始，元空间替代了永久代。元空间不再使用堆内存，而是使用本地内存来存储类的元数据。它的内存可以动态扩展，避免了永久代中的内存溢出问题。</li>
</ul>
</li>
</ol>
<h1><span id="8类加载">8.类加载</span></h1><p>Java 类加载机制是Java运行时环境中的一个核心特性，它负责从文件系统加载.class文件到JVM（Java虚拟机）中。Java 类加载过程主要分为以下几个阶段：</p>
<ol>
<li><p><strong>加载（Loading）</strong>：</p>
<ul>
<li>这个阶段是类加载过程的第一步，虚拟机通过类加载器（ClassLoader）找到类文件，然后读取类文件的二进制数据，并将其转换为方法区的运行时数据结构。</li>
</ul>
</li>
<li><p><strong>验证（Verification）</strong>：</p>
<ul>
<li>验证阶段确保加载的类信息符合JVM规范，没有安全问题。例如，确保类的结构正确，没有违反JVM规范的地方。</li>
</ul>
</li>
<li><p><strong>准备（Preparation）</strong>：</p>
<ul>
<li>准备阶段是为类变量分配内存并设置初始值的过程。这里的初始值指的是数据类型的零值，比如int类型变量会被初始化为0，引用类型变量会被初始化为null。</li>
</ul>
</li>
<li><p><strong>解析（Resolution）</strong>：</p>
<ul>
<li>解析阶段是将类、接口、类成员等的符号引用转换为直接引用的过程。符号引用是类文件中的一个名字，而直接引用是指向内存中的地址。</li>
</ul>
</li>
<li><p><strong>初始化（Initialization）</strong>：</p>
<ul>
<li>初始化是类加载过程中最后一个阶段，这个阶段主要是执行一些初始化的操作。</li>
</ul>
</li>
</ol>
<p><strong>JVM 启动的时候，并不会一次性加载所有的类，而是根据需要去动态加载。</strong>也就是说，大部分类在具体用到的时候才会去加载，这样对内存更加友好。对于已经加载的类会被放在 <code>ClassLoader</code> 中。在类加载的时候，系统会首先判断当前类是否被加载过。已经被加载的类会直接返回，否则才会尝试加载。也就是说，对于一个类加载器来说，相同二进制名称的类只会被加载一次。</p>
<h1><span id="9双亲委派模型">9.双亲委派模型</span></h1><p>双亲委派模型是JVM类加载机制中的一项重要设计，它规定了类加载器的工作过程。该模型的核心思想是：一个类加载器在加载类时，首先把请求委派给父类加载器去处理，只有当父类加载器无法完成这个请求时，才由当前类加载器自己去加载。</p>
<p><strong>工作原理：</strong></p>
<ol>
<li><strong>委派加载</strong>：当一个类加载器（如应用程序类加载器）接到加载类的请求时，首先将这个请求委派给它的父加载器去处理。</li>
<li><strong>递归调用</strong>：父加载器也会遵循同样的委派机制，将请求传递给它的父加载器，直到传递到顶层的启动类加载器（Bootstrap ClassLoader）。</li>
<li><strong>加载类</strong>：启动类加载器尝试加载类。如果成功，则返回加载的类；如果失败，逐级返回，直到回到原始请求的类加载器。</li>
<li><strong>自加载</strong>：如果所有父加载器都无法加载该类，原始请求的类加载器才会尝试自己加载该类。</li>
</ol>
<p>双亲委派模型保证了 Java 程序的稳定运行，可以避免类的重复加载，也保证了 Java 的核心 API 不被篡改。</p>
<p>如果没有使用双亲委派模型，而是每个类加载器加载自己的话就会出现一些问题，比如我们编写一个称为 <code>java.lang.Object</code> 类的话，那么程序运行的时候，系统就会出现两个不同的 <code>Object</code> 类。双亲委派模型可以保证加载的是 JRE 里的那个 <code>Object</code> 类，而不是你写的 <code>Object</code> 类。这是因为 <code>AppClassLoader</code> 在加载你的 <code>Object</code> 类时，会委托给 <code>ExtClassLoader</code> 去加载，而 <code>ExtClassLoader</code> 又会委托给 <code>BootstrapClassLoader</code>，<code>BootstrapClassLoader</code> 发现自己已经加载过了 <code>Object</code> 类，会直接返回，不会去加载你写的 <code>Object</code> 类。</p>
<blockquote>
<p>JVM 中内置了三个重要的 <code>ClassLoader</code>：</p>
<ol>
<li>**<code>BootstrapClassLoader</code>(启动类加载器)**：最顶层的加载类，由 C++实现，通常表示为 null，并且没有父级，主要用来加载 JDK 内部的核心类库（ <code>%JAVA_HOME%/lib</code>目录下的 <code>rt.jar</code>、<code>resources.jar</code>、<code>charsets.jar</code>等 jar 包和类）以及被 <code>-Xbootclasspath</code>参数指定的路径下的所有类。</li>
<li>**<code>ExtensionClassLoader</code>(扩展类加载器)**：主要负责加载 <code>%JRE_HOME%/lib/ext</code> 目录下的 jar 包和类以及被 <code>java.ext.dirs</code> 系统变量所指定的路径下的所有类。</li>
<li>**<code>AppClassLoader</code>(应用程序类加载器)**：面向我们用户的加载器，负责加载当前应用 classpath 下的所有 jar 包和类。</li>
</ol>
</blockquote>

        </article>
        <section class="post-near">
            <ul>
                
                    <li>上一篇: <a href="/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/">设计模式</a></li>
                
                
                    <li>下一篇: <a href="/Java%E9%9B%86%E5%90%88/">Java集合</a></li>
                
            </ul>
        </section>
        
    
        <section class="post-author">
        
            <figure class="author-avatar">
                <img src="https://sdn.geekzu.org/avatar/d22eb460ecab37fcd7205e6a3c55c228?s=200&r=X&d=" alt="Hingle" />
            </figure>
        
            <div class="author-info">
                <h4>Hingle</h4>
                <p>请在这里设置你的作者信息</p>
            </div>
        </section>
    
    </div>
</main>

    <footer>
    <div class="buttons">
        <a class="to-top" href="#"></a>
    </div>
    <div class="wrap min">
        <section class="widget">
            <div class="row">
                <div class="col-m-4">
                    <h3 class="title-recent">最新文章：</h3>
                    <ul class="post-list"><li class="post-list-item"><a class="post-list-link" href="/%E9%A1%B9%E7%9B%AE/">项目</a></li><li class="post-list-item"><a class="post-list-link" href="/MySQL2/">MySQL2</a></li><li class="post-list-item"><a class="post-list-link" href="/Java17%E6%96%B0%E7%89%B9%E6%80%A7/">Java17新特性</a></li><li class="post-list-item"><a class="post-list-link" href="/Redis2/">Redis2</a></li><li class="post-list-item"><a class="post-list-link" href="/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C2/">计算机网络2</a></li><li class="post-list-item"><a class="post-list-link" href="/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F2/">操作系统2</a></li></ul>
                </div>
            </div>
        </section>
        <section class="sub-footer">
            <p>© 2024 <a href="/">测开、Java后端最常见、最核心的面试题总结</a>. All Rights Reserved. Theme By <a href="https://github.com/Dreamer-Paul/Hingle" target="_blank" rel="nofollow">Hingle</a>.</p>
        </section>
    </div>
</footer>


<script src="/static/kico.js"></script>
<script src="/static/hingle.js"></script>


<script>var hingle = new Paul_Hingle({"copyright":false,"night":false});</script>

  </body>
</html>
